<template>
  <div ref="container" class="container"></div>
  <!--  弹出层-->
  <div class="tooltip-box" :style="tooltipPosition" ref="tooltipBox">
    <div class="wrapper">
      <div class="name">标题：{{ tooltipContent.name }}</div>
      <div class="explain">说明：{{ tooltipContent.description }}</div>
    </div>
  </div>
</template>
<script setup lang="ts">
import * as THREE from "three";
import { ref, onMounted, Ref } from "vue";
import { OrbitControls } from "three/addons/controls/OrbitControls.js";
import gsap from "gsap";
// gat.dui 库  图形化控制页面
import * as dat from "dat.gui";

//初始换场景
const scene = new THREE.Scene();

//创建相机
const camera = new THREE.PerspectiveCamera(
  75,
  window.innerWidth / window.innerHeight,
  0.1,
  1000
);

//设置相机的位置
camera.position.set(0, 0, 0.01); //轨道0.01才能动

//矩形里面看房  第一个房间 客厅
//房屋照片集合  材质
const materials: THREE.MeshBasicMaterial[] = []; //右左上下前后
const keTingPhotos = [
  "./images/rectImages/right.webp",
  "./images/rectImages/left.webp",
  "./images/rectImages/top.webp",
  "./images/rectImages/down.webp",
  "./images/rectImages/front.webp",
  "./images/rectImages/back.webp",
];
//一间屋子有  上下左右前后  六个面  所以创建6面墙的图片   现在是放在一个正方体里面了   （球体的话要全景相机拍摄）
keTingPhotos.forEach((item) => {
  const texture = new THREE.TextureLoader().load(item);
  materials.push(new THREE.MeshBasicMaterial({ map: texture }));
});

//创建几何体   宽高深  据图文档可以启动three源码查看docs文档
const geomertry = new THREE.BoxGeometry(10, 10, 10);

//创建材质  vr看房，后面还是图片包裹  材质随意设置 看中文
// const material = new THREE.MeshBasicMaterial({ color: 0x00ff00 }); //材质放背景图以后不要了

//创建网格
const box = new THREE.Mesh(geomertry, materials);
box.geometry.scale(1, 1, -1); //反转z轴 把图片贴到里面去

// 放入场景
scene.add(box);

// 第二个房间卧室
//房屋照片集合  材质
const bedMaterials: THREE.MeshBasicMaterial[] = []; //右左上下前后
const bedRoomPhotos = [
  "./images/bedRoom/right.png",
  "./images/bedRoom/left.png",
  "./images/bedRoom/top.webp",
  "./images/bedRoom/down.png",
  "./images/bedRoom/front.png",
  "./images/bedRoom/back.png",
];
bedRoomPhotos.forEach((item) => {
  const texture = new THREE.TextureLoader().load(item);
  bedMaterials.push(new THREE.MeshBasicMaterial({ map: texture }));
});
const bedV3 = new THREE.Vector3(0, 0, -10); //在坐标轴的位置  xyz
const bedE3 = new THREE.Euler(0, 0, 0); //沿哪个轴旋转。
const bedGeomertry = new THREE.BoxGeometry(10, 10, 10);
const bedBox = new THREE.Mesh(bedGeomertry, bedMaterials);
bedBox.geometry.scale(1, 1, -1);
bedBox.position.copy(bedV3);
bedBox.rotation.copy(bedE3);
scene.add(bedBox);

// 定义卧室  canvas 材质  可以是图片 也可以是 canvas
const canvas = document.createElement("canvas");
canvas.width = 1024;
canvas.height = 1024;
const context = canvas.getContext("2d");
context.fillStyle = "rgba(100,100,100,.7)";
context.fillRect(0, 256, canvas.width, canvas.height / 2);
context.textAlign = "center";
context.textBaseline = "middle";
context.font = "bold 50px  Arial";
context.fillStyle = "#fff";
context.fillText("卧室", canvas.width / 2, canvas.height / 2);

const bedCanvasTexture = new THREE.CanvasTexture(canvas);

//创建卧室导航

const bedSpriteMaterial = new THREE.SpriteMaterial({
  map: bedCanvasTexture,
  transparent: true,
});
const bedSprite = new THREE.Sprite(bedSpriteMaterial);
// 定义canvas位置
const bedSpritePosition = new THREE.Vector3(0, 0, -4);
bedSprite.position.copy(bedSpritePosition);
bedSprite.userData = {
  type: "clickMove",
  position: {
    x: 0,
    y: 0,
    z: -10,
  },
};
scene.add(bedSprite);

const bedToKeTing = () => {
  // 定义客厅  canvas 材质  可以是图片 也可以是 canvas
  const canvas = document.createElement("canvas");
  canvas.width = 1024;
  canvas.height = 1024;
  const context = canvas.getContext("2d");
  context.fillStyle = "rgba(100,100,100,.7)";
  context.fillRect(0, 256, canvas.width, canvas.height / 2);
  context.textAlign = "center";
  context.textBaseline = "middle";
  context.font = "bold 50px  Arial";
  context.fillStyle = "#fff";
  context.fillText("客厅", canvas.width / 2, canvas.height / 2);
  const bedCanvasTexture = new THREE.CanvasTexture(canvas);
  const bedSpriteMaterial = new THREE.SpriteMaterial({
    map: bedCanvasTexture,
    transparent: true,
  });
  const bedSprite = new THREE.Sprite(bedSpriteMaterial);
  // 定义canvas位置
  const bedSpritePosition = new THREE.Vector3(3, -0, -6);
  bedSprite.position.copy(bedSpritePosition);
  bedSprite.userData = {
    type: "clickMove",
    position: {
      x: 0,
      y: 0,
      z: 0.01,
    },
  };
  scene.add(bedSprite);
  return bedSprite;
};
const bedToKeTingSprite = bedToKeTing();

// 弹出层
const tooltipBox = ref<HTMLElement | null>(null);
const tooltipPosition = ref({
  //其实就和我自己写的鼠标右键事件一样 只是难点在于 三维坐标转 二维
  left: "-100%",
  top: "-100%",
});
const tooltipContent: Ref<Record<string, any>> = ref({
  name: "",
  description: "",
});

// 创建信息点，鼠标浮入的时候提示信息
const informationTexture = new THREE.TextureLoader().load("./images/dot.jpg");
const informationSpriteMaterial = new THREE.SpriteMaterial({
  map: informationTexture,
  transparent: true,
});
const informationSprite = new THREE.Sprite(informationSpriteMaterial);
const informationSpritePosition = new THREE.Vector3(1.5, 0.05, -3);
informationSprite.position.copy(informationSpritePosition);
informationSprite.scale.set(0.2, 0.2, 0.2); // 控制图片大小
informationSprite.userData = {
  type: "information",
  name: "信息点名称",
  description: "信息点描述",
};
scene.add(informationSprite);

const poiObjects: THREE.Sprite[] = [];
poiObjects.push(bedSprite);
poiObjects.push(informationSprite); //加入信息点 数组
poiObjects.push(bedToKeTingSprite); //加入卧室门通往客厅
// canvas点击  添加点击事件  光线投射 就是捕获鼠标
const raycaster = new THREE.Raycaster();
const pointer = new THREE.Vector3();

const tooltipShow = (event: MouseEvent) => {
  //
  event.preventDefault();
  //将鼠标位置归一化为设备坐标。的取值范围是（-1 to +1）  就是获取当前鼠标位置
  pointer.x = (event.clientX / window.innerWidth) * 2 - 1;
  pointer.y = -(event.clientY / window.innerHeight) * 2 + 1;

  //通过摄像机和鼠标位置更新射线
  raycaster.setFromCamera(pointer, camera);

  //计算物体和射线的交叉点
  const intersects = raycaster.intersectObjects(poiObjects);
  if (intersects.length > 0) {
    const data = intersects[0].object;
    if (data.userData.type === "information") {
      // 最重要的是 三维世界坐标 转 浏览器现实坐标   Vector3 的project 方法  三维---》（NDC）空间
      // 获取鼠标位置  top left
      // 三维转 NDC屏幕中的坐标  NDC是一个  [-1,1] 之间的坐标系
      // DNC坐标转 屏幕做标  比如下面这样的例子
      // const vector = new THREE.Vector3(10,20,30)
      // const NDC = vector.project(camera);
      //通过下面的公式可以将 NDC坐标转换位屏幕坐标  width和height是默认全屏的canvas
      // x= (NDC.x+1)*(width/2)
      // y= (-NDC.y+1)*(height/2)
      const element = event.target as HTMLElement; //canvas整个屏幕
      const elementWidth = element.clientWidth / 2;
      const elementHeight = element.clientHeight / 2;
      const vector = new THREE.Vector3(
        data.position.x,
        data.position.y,
        data.position.z
      );
      const NDC = vector.project(camera);
      let left = Math.round(
        (NDC.x + 1) * elementWidth - tooltipBox.value!.clientWidth / 2
      );
      let top = Math.round(
        (-NDC.y + 1) * elementHeight - tooltipBox.value!.clientHeight / 2
      );
      tooltipPosition.value = {
        left: `${left}px`,
        top: `${top}px`,
      };
      tooltipContent.value = data.userData;
    }
  }
};
const handleTooltipHide = (event: MouseEvent) => {
  event.preventDefault();
  tooltipPosition.value = {
    left: "-100%",
    top: "-100%",
  };
  tooltipContent.value = {
    name: "",
    description: "",
  };
};

window.addEventListener("click", (event: MouseEvent) => {
  event.preventDefault();
  //将鼠标位置归一化为设备坐标。的取值范围是（-1 to +1）  就是获取当前鼠标位置
  pointer.x = (event.clientX / window.innerWidth) * 2 - 1;
  pointer.y = -(event.clientY / window.innerHeight) * 2 + 1;

  //通过摄像机和鼠标位置更新射线
  raycaster.setFromCamera(pointer, camera);

  //计算物体和射线的交叉点
  const intersects = raycaster.intersectObjects(poiObjects);
  if (intersects.length > 0) {
    //判断有没有交叉
    const data = intersects[0].object.userData;
    if (data.type === "clickMove") {
      gsap.to(camera.position, data.position);
    }
  }
});

// 房间1移动到房间二  本质就是相机的移动  可以利用图形化控制工具看效果
//图形化控制页面
const gui = new dat.GUI();
const boxFolder = gui.addFolder("立方体属性"); //分组
boxFolder.add(box.position, "x").name("移动x轴").min(-5).max(5).step(0.1);
boxFolder.add(box.position, "z").name("移动z轴").min(-5).max(5).step(0.1);
boxFolder.open(); //初始化展开

//结合
const params = {
  visible: true,
  moveAnimation: () => {
    gsap.to(box.position, { x: 5, duration: 2 });
  },
  cameraMoveAnimation: () => {
    gsap.to(camera.position, { x: 0, y: 0, z: -10 });
  },
};

boxFolder
  .add(params, "visible")
  .name("是否可见")
  .onChange((e) => {
    box.visible = e;
  });
boxFolder.add(params, "moveAnimation").name("移动动画");

const cameraFolder = gui.addFolder("相机属性");
cameraFolder.add(params, "cameraMoveAnimation").name("相机移动");
cameraFolder
  .add(camera.position, "x")
  .name("相机的x轴")
  .min(-30)
  .max(30)
  .step(0.1);
cameraFolder
  .add(camera.position, "y")
  .name("相机的y轴")
  .min(-30)
  .max(30)
  .step(0.1);
cameraFolder
  .add(camera.position, "z")
  .name("相机的z轴")
  .min(-30)
  .max(30)
  .step(0.1);
cameraFolder
  .add(camera.rotation, "z")
  .name("相机z轴旋转")
  .min(-5)
  .max(5)
  .step(0.1);

cameraFolder.open();
//球体看房  需要专业的相机拍摄
const useSphere = () => {
  const sphereGeometry = new THREE.SphereGeometry(1, 50, 50); //半径 经纬上的分段数
  const texture = new THREE.TextureLoader().load("./images/qiuTi.jpg");
  const sphereMaterial = new THREE.MeshBasicMaterial({ map: texture });
  const sphere = new THREE.Mesh(sphereGeometry, sphereMaterial);
  sphere.geometry.scale(1, 1, -1);
  scene.add(sphere);
};

// useRect(); //矩形看房
// useSphere(); //球形看房
// 视口监听
window.addEventListener("resize", () => {
  //随着视口变化更新宽高比
  camera.aspect = window.innerWidth / window.innerHeight;
  // 更新相机的投影矩阵
  camera.updateProjectionMatrix();
  // 更新渲染前的大小
  renderer.setSize(window.innerWidth, window.innerHeight);
});

//添加坐标辅助器
const axesHelper = new THREE.AxesHelper(20); //参数是轴线长度
scene.add(axesHelper);
//控制器
let controls: OrbitControls;
// 创建渲染器
const renderer = new THREE.WebGLRenderer();

//设置渲染器的大小
renderer.setSize(window.innerWidth, window.innerHeight);

const container = ref<HTMLElement | null>(null);

const render = () => {
  renderer.render(scene, camera);
  requestAnimationFrame(render);
  //requestAnimationFrame: 在浏览器下一次重绘之前调用指定的回调函数，确保动画与页面刷新同步，从而实现更流畅的动画效果。
  // 相比 setTimeout 或 setInterval，requestAnimationFrame 更加效率高，能自动调整动画帧率（通常是 60fps），并在标签切换或页面隐藏时自动暂停。
  // 在函数内可以更新元素位置、样式等，实现动画效果
  // 不断调用自我（递归）实现连续动画
};
onMounted(() => {
  if (container.value) {
    //轨道控制  下面的鼠标控制启用了
    /*    controls = new OrbitControls(camera, renderer.domElement);
    controls.enableDamping = true; //阻尼感
    controls.update();*/
    container.value.appendChild(renderer.domElement);
    render();

    //控制视角移动的浮动范围
    let isMouseDown = false;
    container.value.addEventListener(
      "mousedown",
      () => {
        isMouseDown = true;
      },
      false
    );
    container.value.addEventListener(
      "mouseup",
      () => {
        isMouseDown = false;
      },
      false
    );
    container.value.addEventListener(
      "mouseout",
      () => {
        isMouseDown = false;
      },
      false
    );
    container.value.addEventListener(
      "mousemove",
      (event) => {
        if (isMouseDown) {
          camera.rotation.x += event.movementY * 0.001; //沿x轴旋转时 是用y垂直方向的移动距离
          camera.rotation.y += event.movementX * 0.001;
          camera.rotation.order = "YXZ"; //设置欧拉角的顺序 表示旋转的顺序 默认是XYZ   拖拽时更加符合人体工学
        }
      },
      false
    );

    //信息点
    renderer.domElement.addEventListener("mousemove", tooltipShow);
    tooltipBox.value!.addEventListener("mouseleave", handleTooltipHide);
  }
});
</script>

<style scoped lang="scss">
.container {
  width: 100%;
  height: 100%;
  background-color: #000;
  overflow: hidden;
}
.tooltip-box {
  position: absolute;
  padding: 0 0 40px 0;
  line-height: 30px;
  border-radius: 5px;
  z-index: 100;
  cursor: pointer;
  .wrapper {
    position: relative;
    //width: 240px;
    //height: 200px;
    padding: 10px;
    background-color: rgba(0, 0, 0, 0.6);
    .name {
      width: 100%;
      padding: 6px 0;
    }
    .explain {
      width: 100%;
      max-height: 100px;
      font-size: 14px;
      overflow-y: auto;
    }
  }
}
</style>
